//the storage engine for appendable, relatively large files, e.g. 1MB
package flatstore

import (
	"errors"
	"io/ioutil"
	"math/rand"
	"os"
	"strconv"
	"syscall"
	"time"
)

const (
	SubdirectoryLimit      = 365
	SingleDirDataFileLimit = 10000

	FileOpenOpt = os.O_RDWR | os.O_CREATE | os.O_APPEND
)

type Store struct {
	dataDir      string
	filePointers *FdCache
}

func NewStore(datadir string, maxCachedFd int) (s *Store, err error) {
	s = new(Store)
	s.dataDir = datadir
	s.filePointers = NewFdCache(maxCachedFd)

	//creat and check all subdirectories
	if err = s.createAndCheckSubdirectories(); err != nil {
		return nil, err
	}

	return s, nil
}

//the parent directory should be fsync to the disk
func (s *Store) createAndCheckSubdirectories() (err error) {
	for subDirN := 1; subDirN <= SubdirectoryLimit; subDirN++ {
		//no need to check if dir exist, and MkdirAll does nothing and returns nil.
		if err = os.MkdirAll(s.dataDir+"/"+strconv.Itoa(subDirN), 0755); err != nil {
			return
		}
		if err = FsyncDir(s.dataDir + "/" + strconv.Itoa(subDirN)); err != nil {
			return
		}
	}
	if err = FsyncDir(s.dataDir); err != nil {
		return
	}

	return
}

func FsyncDir(dir string) (err error) {
	fp, err := syscall.Open(dir, os.O_RDONLY, 0666)
	if err != nil {
		return
	}
	if err = syscall.Fsync(fp); err != nil {
		return
	}

	return
}

//get free space information via fsstat etc.
func (s *Store) StatFreeSpace() int64 {
	var buf syscall.Statfs_t

	err := syscall.Statfs(s.dataDir, &buf)
	if err != nil {
		return 0
	}

	return int64(buf.Bfree) * buf.Bsize
}

func (s *Store) GenerateFile() (subdir, file int, err error) {
	rand.Seed(time.Now().UTC().UnixNano())

	var fp *os.File
	dirRetry := 0

	for {
		dirRetry = dirRetry + 1
		if dirRetry > SubdirectoryLimit {
			err = errors.New("GenerateFileError")
			return
		}

		subdir = rand.Intn(SubdirectoryLimit) + 1
		file = rand.Intn(SingleDirDataFileLimit) + 1
		fp, err = os.OpenFile(s.dataDir+"/"+strconv.Itoa(subdir)+"/"+strconv.Itoa(file), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
		if err == nil {
			break
		}
	}

	index := subdir*SingleDirDataFileLimit + file
	//add to cache
	s.filePointers.Add(index, fp, 0)

	return
}

func (s *Store) CreateAndAppend(subdir, file int, data []byte) (err error) {
	fp, err := os.OpenFile(s.dataDir+"/"+strconv.Itoa(subdir)+"/"+strconv.Itoa(file), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
	if err != nil {
		return
	}
	if _, err = fp.Write(data); err != nil {
		return
	}

	index := subdir*SingleDirDataFileLimit + file
	fileInfo, err := fp.Stat()
	if err != nil {
		return err
	}
	//add to cache
	s.filePointers.Add(index, fp, fileInfo.Size())

	return
}

func (s *Store) Append(subdir, file int, offset int64, data []byte) (err error) {
	index := subdir*SingleDirDataFileLimit + file
	fp, fileLen, ok := s.filePointers.Get(index)
	if !ok {
		fp, fileLen, err = s.openFile(subdir, file)
		if err != nil {
			return
		}
	}
	if offset != fileLen {
		return errors.New("FileSizeMismatchError")
	}

	n, err := fp.Write(data)
	if err != nil {
		return
	}
	//add to cache
	s.filePointers.Add(index, fp, fileLen+int64(n))

	return
}

func (s *Store) Fsync(subdir, file int) (err error) {
	index := subdir*SingleDirDataFileLimit + file
	fp, fileLen, ok := s.filePointers.Get(index)
	if !ok {
		if fp, fileLen, err = s.openFile(subdir, file); err != nil {
			return
		}
	}
	//add to cache
	s.filePointers.Add(index, fp, fileLen)

	return fp.Sync()
}

func (s *Store) Read(subdir, file int, offset, length int64, data []byte) (nbuf []byte, err error) {
	index := subdir*SingleDirDataFileLimit + file
	fp, fileLen, ok := s.filePointers.Get(index)
	if !ok {
		if fp, fileLen, err = s.openFile(subdir, file); err != nil {
			return
		}
	}
	//add to cache
	s.filePointers.Add(index, fp, fileLen)
	if offset+length > fileLen || offset < 0 || length < 0 {
		return nil, errors.New("FileSizeMismatchError")
	}

	if data == nil || int64(len(data)) != length {
		data = make([]byte, length)
	}
	if _, err = fp.ReadAt(data, offset); err != nil {
		return nil, err
	}

	nbuf = data
	return
}

func (s *Store) openFile(subdir, file int) (fp *os.File, size int64, err error) {
	fp, err = os.OpenFile(s.dataDir+"/"+strconv.Itoa(subdir)+"/"+strconv.Itoa(file), os.O_RDWR, 0666)
	if err != nil {
		return nil, 0, err
	}

	fileInfo, err := fp.Stat()
	if err != nil {
		return fp, 0, err
	}
	size = fileInfo.Size()

	return
}

func (s *Store) Delete(subdir, file int) error {
	s.filePointers.Delete(subdir*SingleDirDataFileLimit + file)
	return os.Remove(s.dataDir + "/" + strconv.Itoa(subdir) + "/" + strconv.Itoa(file))
}

//snapshot the file list of the given subdirectory as lines of fileId/size, e.g.
//	1/10240000\r\n
//	8/250000000\r\n
//	...
//  \r\n
func (s *Store) DirSnapshot(subdir int) (buf []byte, err error) {
	fileInfoList, err := ioutil.ReadDir(s.dataDir + "/" + strconv.Itoa(subdir))
	if err != nil {
		return nil, err
	}
	for i := 0; i < len(fileInfoList); i++ {
		infoByte := []byte(fileInfoList[i].Name() + "/" + strconv.Itoa(int(fileInfoList[i].Size())) + "\r\n")
		buf = append(buf, infoByte...)
	}
	endStr := "\r\n"
	buf = append(buf, []byte(endStr)...)

	return
}
